allow DataOutputAgent to build complex XML

Andrew Cantino 9 years ago
parent
commit
136aaea04b

+ 84 - 6
app/models/agents/data_output_agent.rb

@@ -22,6 +22,21 @@ module Agents
22 22
           * `template` - A JSON object representing a mapping between item output keys and incoming event values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. The `item` key will be repeated for every Event. The `pubDate` key for each item will have the creation time of the Event unless given.
23 23
           * `events_to_show` - The number of events to output in RSS or JSON. (default: `40`)
24 24
           * `ttl` - A value for the <ttl> element in RSS output. (default: `60`)
25
+
26
+        If you'd like to output RSS tags with attributes, such as `enclosure`, use something like the following in your `template`:
27
+
28
+            "enclosure" => {
29
+              "_attributes" => {
30
+                "type" => "audio/mpeg",
31
+                "url" => "{{media_url}}"
32
+              }
33
+            },
34
+            "tag" => {
35
+              "_attributes" => {
36
+                "key" => "value"
37
+              },
38
+              "_contents" => "tag contents (can be an object for nesting)"
39
+            }
25 40
       MD
26 41
     end
27 42
 
@@ -35,15 +50,12 @@ module Agents
35 50
           "item" => {
36 51
             "title" => "{{title}}",
37 52
             "description" => "Secret hovertext: {{hovertext}}",
38
-            "link" => "{{url}}",
53
+            "link" => "{{url}}"
39 54
           }
40 55
         }
41 56
       }
42 57
     end
43 58
 
44
-    #"guid" => "",
45
-    #  "pubDate" => ""
46
-
47 59
     def working?
48 60
       last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
49 61
     end
@@ -104,7 +116,7 @@ module Agents
104 116
             'title' => feed_title,
105 117
             'description' => feed_description,
106 118
             'pubDate' => Time.now,
107
-            'items' => items
119
+            'items' => simplify_item_for_json(items)
108 120
           }
109 121
 
110 122
           return [content, 200]
@@ -122,7 +134,7 @@ module Agents
122 134
 
123 135
           XML
124 136
 
125
-          content += items.to_xml(:skip_types => true, :root => "items", :skip_instruct => true, :indent => 1).gsub(/^<\/?items>/, '').strip
137
+          content += simplify_item_for_xml(items).to_xml(skip_types: true, root: "items", skip_instruct: true, indent: 1).gsub(/^<\/?items>/, '').strip
126 138
 
127 139
           content += Utils.unindent(<<-XML)
128 140
             </channel>
@@ -139,5 +151,71 @@ module Agents
139 151
         end
140 152
       end
141 153
     end
154
+
155
+    private
156
+
157
+    class XMLNode
158
+      def initialize(tag_name, attributes, contents)
159
+        @tag_name, @attributes, @contents = tag_name, attributes, contents
160
+      end
161
+
162
+      def to_xml(options)
163
+        if @contents.is_a?(Hash)
164
+          options[:builder].tag! @tag_name, @attributes do
165
+            @contents.each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options.merge(skip_instruct: true)) }
166
+          end
167
+        else
168
+          options[:builder].tag! @tag_name, @attributes, @contents
169
+        end
170
+      end
171
+    end
172
+
173
+    def simplify_item_for_xml(item)
174
+      if item.is_a?(Hash)
175
+        item.each.with_object({}) do |(key, value), memo|
176
+          if value.is_a?(Hash)
177
+            if value.key?('_attributes') || value.key?('_contents')
178
+              memo[key] = XMLNode.new(key, value['_attributes'], simplify_item_for_xml(value['_contents']))
179
+            else
180
+              memo[key] = simplify_item_for_xml(value)
181
+            end
182
+          else
183
+            memo[key] = value
184
+          end
185
+        end
186
+      elsif item.is_a?(Array)
187
+        item.map { |value| simplify_item_for_xml(value) }
188
+      else
189
+        item
190
+      end
191
+    end
192
+
193
+    def simplify_item_for_json(item)
194
+      if item.is_a?(Hash)
195
+        item.each.with_object({}) do |(key, value), memo|
196
+          if value.is_a?(Hash)
197
+            if value.key?('_attributes') || value.key?('_contents')
198
+              contents = if value['_contents'] && value['_contents'].is_a?(Hash)
199
+                           simplify_item_for_json(value['_contents'])
200
+                         elsif value['_contents']
201
+                           { "contents" => value['_contents'] }
202
+                         else
203
+                           {}
204
+                         end
205
+
206
+              memo[key] = contents.merge(value['_attributes'] || {})
207
+            else
208
+              memo[key] = simplify_item_for_json(value)
209
+            end
210
+          else
211
+            memo[key] = value
212
+          end
213
+        end
214
+      elsif item.is_a?(Array)
215
+        item.map { |value| simplify_item_for_json(value) }
216
+      else
217
+        item
218
+      end
219
+    end
142 220
   end
143 221
 end

+ 3 - 1
app/models/agents/wunderlist_agent.rb

@@ -53,7 +53,9 @@ module Agents
53 53
         end
54 54
       end
55 55
     end
56
-  private
56
+
57
+    private
58
+
57 59
     def request_guard(&blk)
58 60
       response = yield
59 61
       error("Error during http request: #{response.body}") if response.code > 400

+ 132 - 1
spec/models/agents/data_output_agent_spec.rb

@@ -83,7 +83,7 @@ describe Agents::DataOutputAgent do
83 83
       expect(status).to eq(200)
84 84
     end
85 85
 
86
-    describe "returning events as RSS and JSON" do
86
+    describe "outputtng events as RSS and JSON" do
87 87
       let!(:event1) do
88 88
         agents(:bob_website_agent).create_event :payload => {
89 89
           "url" => "http://imgs.xkcd.com/comics/evolving.png",
@@ -194,5 +194,136 @@ describe Agents::DataOutputAgent do
194 194
         })
195 195
       end
196 196
     end
197
+
198
+    describe "outputting nesting" do
199
+      before do
200
+        agent.options['template']['item']['enclosure'] = {
201
+          "_attributes" => {
202
+            "type" => "audio/mpeg",
203
+            "url" => "{{media_url}}"
204
+          },
205
+          "_self_closing" => "true"
206
+        }
207
+        agent.options['template']['item']['foo'] = {
208
+          "_attributes" => {
209
+            "attr" => "attr-value-{{foo}}"
210
+          },
211
+          "_contents" => "Foo: {{foo}}"
212
+        }
213
+        agent.options['template']['item']['nested'] = {
214
+          "_attributes" => {
215
+            "key" => "value"
216
+          },
217
+          "_contents" => {
218
+            "title" => "some title"
219
+          }
220
+        }
221
+        agent.options['template']['item']['simpleNested'] = {
222
+          "title" => "some title",
223
+          "complex" => {
224
+            "_attributes" => {
225
+              "key" => "value"
226
+            },
227
+            "_contents" => {
228
+              "first" => {
229
+                "_attributes" => {
230
+                  "a" => "b"
231
+                },
232
+                "_contents" => {
233
+                  "second" => "value"
234
+                }
235
+              }
236
+            }
237
+          }
238
+        }
239
+        agent.save!
240
+      end
241
+
242
+      let!(:event) do
243
+        agents(:bob_website_agent).create_event :payload => {
244
+          "url" => "http://imgs.xkcd.com/comics/evolving.png",
245
+          "title" => "Evolving",
246
+          "hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.",
247
+          "media_url" => "http://google.com/audio.mpeg",
248
+          "foo" => 1
249
+        }
250
+      end
251
+
252
+      it "can output JSON" do
253
+        content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
254
+        expect(status).to eq(200)
255
+        expect(content['items'].first).to eq(
256
+          {
257
+            'title' => 'Evolving',
258
+            'description' => 'Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.',
259
+            'link' => 'http://imgs.xkcd.com/comics/evolving.png',
260
+            'guid' => event.id,
261
+            'pubDate' => event.created_at.rfc2822,
262
+            'enclosure' => {
263
+              "type" => "audio/mpeg",
264
+              "url" => "http://google.com/audio.mpeg"
265
+            },
266
+            'foo' => {
267
+              'attr' => 'attr-value-1',
268
+              'contents' => 'Foo: 1'
269
+            },
270
+            'nested' => {
271
+              "key" => "value",
272
+              "title" => "some title"
273
+            },
274
+            'simpleNested' => {
275
+              "title" => "some title",
276
+              "complex" => {
277
+                "key"=>"value",
278
+                "first" => {
279
+                  "a" => "b",
280
+                  "second"=>"value"
281
+                }
282
+              }
283
+            }
284
+          }
285
+        )
286
+      end
287
+
288
+      it "can output RSS" do
289
+        stub(agent).feed_link { "https://yoursite.com" }
290
+        content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
291
+        expect(status).to eq(200)
292
+        expect(content_type).to eq('text/xml')
293
+        expect(content.gsub(/\s+/, '')).to eq Utils.unindent(<<-XML).gsub(/\s+/, '')
294
+          <?xml version="1.0" encoding="UTF-8" ?>
295
+          <rss version="2.0">
296
+          <channel>
297
+           <title>XKCD comics as a feed</title>
298
+           <description>This is a feed of recent XKCD comics, generated by Huginn</description>
299
+           <link>https://yoursite.com</link>
300
+           <lastBuildDate>#{Time.now.rfc2822}</lastBuildDate>
301
+           <pubDate>#{Time.now.rfc2822}</pubDate>
302
+           <ttl>60</ttl>
303
+
304
+           <item>
305
+             <title>Evolving</title>
306
+             <description>Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.</description>
307
+             <link>http://imgs.xkcd.com/comics/evolving.png</link>
308
+             <pubDate>#{event.created_at.rfc2822}</pubDate>
309
+             <enclosure type="audio/mpeg" url="http://google.com/audio.mpeg" />
310
+             <foo attr="attr-value-1">Foo: 1</foo>
311
+             <nested key="value"><title>some title</title></nested>
312
+             <simpleNested>
313
+               <title>some title</title>
314
+               <complex key="value">
315
+                 <first a="b">
316
+                   <second>value</second>
317
+                 </first>
318
+               </complex>
319
+             </simpleNested>
320
+             <guid>#{event.id}</guid>
321
+           </item>
322
+
323
+          </channel>
324
+          </rss>
325
+        XML
326
+      end
327
+    end
197 328
   end
198 329
 end

+ 2 - 1
spec/models/agents/wunderlist_agent_spec.rb

@@ -54,8 +54,9 @@ describe Agents::WunderlistAgent do
54 54
 
55 55
   describe "#receive" do
56 56
     it "send a message to the hipchat" do
57
-      stub_request(:post, 'https://a.wunderlist.com/api/v1/tasks').with { |request| request.body == 'abc'}
57
+      stub_request(:post, 'https://a.wunderlist.com/api/v1/tasks')
58 58
       @checker.receive([@event])
59
+      expect(WebMock).to have_requested(:post, "https://a.wunderlist.com/api/v1/tasks")
59 60
     end
60 61
   end
61 62